Lambda-backedカスタムリソースではリクエストタイプごとの処理を意識しよう
最近Lambda-backedカスタムリソースを使い始めたんですが、Lambdaに記述するリクエストタイプが分からず時間を浪費してしまったのでお勉強したことをまとめておきます。
カスタムリソースは非常に便利ですが、テンプレート外でいろいろできてしまうため諸刃の剣とも言われます。CloudFormationでサポートされていないものを補うために利用したいという方はしっかりと理解した上で使っていきましょう。
カスタムリソースとは
CloudFormationからLambdaやSNSを動かし、その結果を返すことで作成完了とするリソースのことです。ちょっと分かりにくいのでイメージ図。
上の図ではCloudFormationからスタックの作成が行われると、Lambdaとそれに必要なロールを作成します。その後カスタムリソースと呼ばれるリソースがLambdaを実行して、その結果をカスタムリソースへ応答オブジェクトとして返却します。
カスタムリソースは実体のないリソースで、テンプレート上でLambdaの実行と結果の取得を定義するためのリソースなので、AWS上には何も作成されない点がちょっと分かりにくいポイントですね。
Lambdaからのレスポンスとなる応答オブジェクトは、JSONの形式で送信されLambdaの成功/失敗や失敗時の理由、CloudFormationテンプレートに返したい任意のデータなどが含まれます。含まれる内容はドキュメントをご参照ください。
カスタムリソースの応答オブジェクト - AWS CloudFormation
Lambdaから応答オブジェクトに組み込んだ値はFn::GetAtt
関数を使いテンプレート内で取得できます。この辺りはチュートリアルで一度使った方が分かりやすいです。
このように、CloudFormation上からプロバイダ(Lambda,SNS)を実行し、応答オブジェクトを取得するリソースのことをカスタムリソースと言います。
リクエストタイプとは
カスタムリソースを利用する際に覚えておきたいのがリクエストタイプです。CloudFormationからカスタムリソースに対して作成・更新・削除したときにLambdaへ送信されるリクエストに含まれるリクエストタイプが変わります。
上記の例では、Lambdaは最新のAMI IDを取得してカスタムリソースへ返す処理を行うものだとします。CloudFormationからスタックの作成(Create)やカスタムリソースの更新(Update)が行われたときには、Lambdaは最新のAMI IDを取得して返すという動きをします。しかしスタックの削除(Delete)の時は何もせずにLambdaの成功をレスポンスして完了とするように動きが変わっています。
このようにCloudFormationスタックへの作成、更新、削除によってLambdaへ送られるリクエストタイプが変わることに注意しましょう。
カスタムリソースのリクエストタイプ - AWS CloudFormation
そのためLambda側ではeventのリクエストタイプに合わせ、どのような処理をしてレスポンスを返すのかを記述しておく必要があります。これを記述しないと、Lambdaの処理が終わったとしてもカスタムリソース側にレスポンスが返らず、スタックのタイムアウト(デフォルトは1時間)になるまで作成や削除が完了しなくなるので気をつけましょう…
カスタムリソースを使ってスタックを作成、削除するときにCREATE_IN_PROGRESS
やDELETE_IN_PROGRESS
の状態から進まない場合があります。そんなときはリクエストタイプごとにレスポンスを返すようLambdaのコードが書けているかを確認してみましょう。
あくまで一例ですが、私はハンドラーに以下のように記述するようにしています。この他にエラーが発生した場合にもレスポンスを返す必要があるので漏れがないようにしましょう。
import cfnresponse def lambda_handler(event, context): if event['RequestType'] == 'Create': # Create時に実行したい処理 cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Delete': # Delete時に実行したい処理 cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Update': # Update時に実行したい処理 cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
cfnresponse
モジュールの使い方についてはドキュメントに記載あります。
以下のエントリも合わせて確認しておくとハマらずに済みます。
リクエストタイプの更新(Update)には注意
リクエストタイプの更新はスタックの更新ではなく「カスタムリソースのプロパティに変更があった場合」カスタムリソースのプロバイダにリクエストが送信されることに注意してください。
Lambdaのコードを変更してスタックを更新したとしても、カスタムリソースのプロパティが変更されていなければリクエストは送信されずにLambdaは実行されません。
カスタムリソースを試してみる
簡単なテンプレートを使ってスタックを作成、更新、削除の動作を確認してみます。
テンプレートの確認
以下のようなサンプルのテンプレートを使ってカスタムリソースを試してみます。
AWSTemplateFormatVersion: "2010-09-09" Resources: SampleLambda: Type: Custom::SampleLambda Properties: ServiceToken: !GetAtt "LambdaFunction.Arn" Hoge: "hoge" LambdaFunction: Type: AWS::Lambda::Function Properties: Role: !GetAtt "LambdaExecutionRole.Arn" Runtime: "python3.8" Handler: index.lambda_handler Timeout: "180" Code: ZipFile: | import cfnresponse def lambda_handler(event, context): hoge = event['ResourceProperties']['Hoge'] if event['RequestType'] == 'Create': print('Create:'+hoge) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Delete': print('Delete:'+hoge) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Update': print('Update:'+hoge) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: sample-lambda-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: arn:aws:logs:*:*:*
SampleLambda
がカスタムリソースでLambdaFunction
を呼び出しています。プロパティの中でHoge: 'hoge'
という値をリクエストに含めるよう定義しています。
LambdaFunction
では簡単なコードを実装していて、カスタムリソースから受け取ったHoge
の値を取得して、リクエストタイプ+Hoge
を出力します。
LambdaExecutionRole
はLambdaにアタッチするロールを定義しています。今回はログを出力するだけなので、最低限の権限のみ付与しています。
スタックの作成
先ほどのテンプレートを使ってスタックを作成してみます。特に特別な設定は必要ないのでデフォルトで作成してみましょう。
ロール、Lambda関数、カスタムリソースの順で作成されているのが確認できます。
Lambdaの実行を確認するため、CloudWatch Logsを確認してみるとリクエストタイプCreate
にカスタムリソースから引き渡されたhoge
の値が出力されました。
スタックの更新
次はスタックの更新を確認してみます。
先ほど注意事項に書いた通りスタックの更新ではLambdaは実行されず、カスタムリソースの更新によって起動されることを確認してみましょう。
Lambdaのコードをハイライトの部分だけ変えて更新してみます。
import cfnresponse def lambda_handler(event, context): hoge = event['ResourceProperties']['Hoge'] if event['RequestType'] == 'Create': print('Create:'+hoge) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Delete': print('Delete:'+hoge) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Update': print('Update!!:'+hoge) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
無事LambdaFunction
は更新されました。
しかし、Lambdaの実行は行われないためメトリクスを確認してもスタック作成時の1回のみで、ログにも更新時のものはありません。
Lambdaのコード変更ではLambdaが実行されないことを確認したので、次にカスタムリソースの変更時の動作を確認してみましょう。
カスタムリソースを定義しているセクションでHoge
の値をhuga
に変更しました。
SampleLambda: Type: Custom::SampleLambda Properties: ServiceToken: !GetAtt "LambdaFunction.Arn" Hoge: "huga"
この状態でスタックの更新をしてみます。カスタムリソースであるSampleLambda
の更新が確認できました。
ログを確認してみると、リクエストタイプUpdate
にカスタムリソースから新しく引き渡されたhuga
の値が出力されました。
スタックの削除
最後にスタックの削除を確認しましょう。特に注意する点はないため、削除のリクエストタイプがリクエストとして送られた時に、ログに出力されるか確認します。
スタックの削除が開始されると、SampleLambdaがDELETE_IN_PROGRESS
となり、このタイミングでLambdaが実行されます。ログを確認するとカスタムリソースが削除される前に、リクエストタイプDelete
でカスタムリソースから引き渡されたhuga
の値が出力されています。
この時cfnresponse.send
を使ったレスポンスを返さないと、カスタムリソースがDELETE_IN_PROGRESS
から進まなくなるということは忘れないようにしましょう。
まとめ
Lambda-backedカスタムリソースを使う時のリクエストタイプについて解説しました。初めてカスタムリソースを使う時にハマりがちなポイントなので、これから使う方は意識してみてください。この記事で一人でもCREATE_IN_PROGRESS
やDELETE_IN_PROGRESS
で1時間待ちぼうけになる人が減れば幸いです。